// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 ******************************************************************************
 * Copyright (C) 2007, International Business Machines Corporation and   *
 * others. All Rights Reserved.                                               *
 ******************************************************************************
 */

package com.ibm.icu.impl.duration;

import com.ibm.icu.impl.duration.impl.DataRecord.ETimeLimit;

/**
 * Represents an approximate duration in multiple TimeUnits. Each unit, if set, has a count (which
 * can be fractional and must be non-negative). In addition Period can either represent the duration
 * as being into the past or future, and as being more or less than the defined value.
 *
 * <p>Use a PeriodFormatter to convert a Period to a String.
 *
 * <p>Periods are immutable. Mutating operations return the new result leaving the original
 * unchanged.
 *
 * <p>Example:
 *
 * <pre>
 * Period p1 = Period.at(3, WEEK).and(2, DAY).inFuture();
 * Period p2 = p1.and(12, HOUR);</pre>
 */
public final class Period {
    final byte timeLimit;
    final boolean inFuture;
    final int[] counts;

    /**
     * Constructs a Period representing a duration of count units extending into the past.
     *
     * @param count the number of units, must be non-negative
     * @param unit the unit
     * @return the new Period
     */
    public static Period at(float count, TimeUnit unit) {
        checkCount(count);
        return new Period(ETimeLimit.NOLIMIT, false, count, unit);
    }

    /**
     * Constructs a Period representing a duration more than count units extending into the past.
     *
     * @param count the number of units. must be non-negative
     * @param unit the unit
     * @return the new Period
     */
    public static Period moreThan(float count, TimeUnit unit) {
        checkCount(count);
        return new Period(ETimeLimit.MT, false, count, unit);
    }

    /**
     * Constructs a Period representing a duration less than count units extending into the past.
     *
     * @param count the number of units. must be non-negative
     * @param unit the unit
     * @return the new Period
     */
    public static Period lessThan(float count, TimeUnit unit) {
        checkCount(count);
        return new Period(ETimeLimit.LT, false, count, unit);
    }

    /**
     * Set the given unit to have the given count. Marks the unit as having been set. This can be
     * used to set multiple units, or to reset a unit to have a new count. This does <b>not</b> add
     * the count to an existing count for this unit.
     *
     * @param count the number of units. must be non-negative
     * @param unit the unit
     * @return the new Period
     */
    public Period and(float count, TimeUnit unit) {
        checkCount(count);
        return setTimeUnitValue(unit, count);
    }

    /**
     * Mark the given unit as not being set.
     *
     * @param unit the unit to unset
     * @return the new Period
     */
    public Period omit(TimeUnit unit) {
        return setTimeUnitInternalValue(unit, 0);
    }

    /**
     * Mark the duration as being at the defined duration.
     *
     * @return the new Period
     */
    public Period at() {
        return setTimeLimit(ETimeLimit.NOLIMIT);
    }

    /**
     * Mark the duration as being more than the defined duration.
     *
     * @return the new Period
     */
    public Period moreThan() {
        return setTimeLimit(ETimeLimit.MT);
    }

    /**
     * Mark the duration as being less than the defined duration.
     *
     * @return the new Period
     */
    public Period lessThan() {
        return setTimeLimit(ETimeLimit.LT);
    }

    /**
     * Mark the time as being in the future.
     *
     * @return the new Period
     */
    public Period inFuture() {
        return setFuture(true);
    }

    /**
     * Mark the duration as extending into the past.
     *
     * @return the new Period
     */
    public Period inPast() {
        return setFuture(false);
    }

    /**
     * Mark the duration as extending into the future if future is true, and into the past
     * otherwise.
     *
     * @param future true if the time is in the future
     * @return the new Period
     */
    public Period inFuture(boolean future) {
        return setFuture(future);
    }

    /**
     * Mark the duration as extending into the past if past is true, and into the future otherwise.
     *
     * @param past true if the time is in the past
     * @return the new Period
     */
    public Period inPast(boolean past) {
        return setFuture(!past);
    }

    /**
     * Returns true if any unit is set.
     *
     * @return true if any unit is set
     */
    public boolean isSet() {
        for (int i = 0; i < counts.length; ++i) {
            if (counts[i] != 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if the given unit is set.
     *
     * @param unit the unit to test
     * @return true if the given unit is set.
     */
    public boolean isSet(TimeUnit unit) {
        return counts[unit.ordinal] > 0;
    }

    /**
     * Returns the count for the specified unit. If the unit is not set, returns 0.
     *
     * @param unit the unit to test
     * @return the count
     */
    public float getCount(TimeUnit unit) {
        int ord = unit.ordinal;
        if (counts[ord] == 0) {
            return 0;
        }
        return (counts[ord] - 1) / 1000f;
    }

    /**
     * Returns true if this represents a duration into the future.
     *
     * @return true if this represents a duration into the future.
     */
    public boolean isInFuture() {
        return inFuture;
    }

    /**
     * Returns true if this represents a duration into the past
     *
     * @return true if this represents a duration into the past
     */
    public boolean isInPast() {
        return !inFuture;
    }

    /**
     * Returns true if this represents a duration in excess of the defined duration.
     *
     * @return true if this represents a duration in excess of the defined duration.
     */
    public boolean isMoreThan() {
        return timeLimit == ETimeLimit.MT;
    }

    /**
     * Returns true if this represents a duration less than the defined duration.
     *
     * @return true if this represents a duration less than the defined duration.
     */
    public boolean isLessThan() {
        return timeLimit == ETimeLimit.LT;
    }

    /**
     * Returns true if rhs extends Period and the two Periods are equal.
     *
     * @param rhs the object to compare to
     * @return true if rhs is a Period and is equal to this
     */
    @Override
    public boolean equals(Object rhs) {
        try {
            return equals((Period) rhs);
        } catch (ClassCastException e) {
            return false;
        }
    }

    /**
     * Returns true if the same units are defined with the same counts, both extend into the future
     * or both into the past, and if the limits (at, more than, less than) are the same. Note that
     * this means that a period of 1000ms and a period of 1sec will not compare equal.
     *
     * @param rhs the period to compare to
     * @return true if the two periods are equal
     */
    public boolean equals(Period rhs) {
        if (rhs != null && this.timeLimit == rhs.timeLimit && this.inFuture == rhs.inFuture) {
            for (int i = 0; i < counts.length; ++i) {
                if (counts[i] != rhs.counts[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Returns the hashCode.
     *
     * @return the hashCode
     */
    @Override
    public int hashCode() {
        int hc = (timeLimit << 1) | (inFuture ? 1 : 0);
        for (int i = 0; i < counts.length; ++i) {
            hc = (hc << 2) ^ counts[i];
        }
        return hc;
    }

    /** Private constructor used by static factory methods. */
    private Period(int limit, boolean future, float count, TimeUnit unit) {
        this.timeLimit = (byte) limit;
        this.inFuture = future;
        this.counts = new int[TimeUnit.units.length];
        this.counts[unit.ordinal] = (int) (count * 1000) + 1;
    }

    /** Package private constructor used by setters and factory. */
    Period(int timeLimit, boolean inFuture, int[] counts) {
        this.timeLimit = (byte) timeLimit;
        this.inFuture = inFuture;
        this.counts = counts;
    }

    /** Set the unit's internal value, converting from float to int. */
    private Period setTimeUnitValue(TimeUnit unit, float value) {
        if (value < 0) {
            throw new IllegalArgumentException("value: " + value);
        }
        return setTimeUnitInternalValue(unit, (int) (value * 1000) + 1);
    }

    /**
     * Sets the period to have the provided value, 1/1000 of the unit plus 1. Thus unset values are
     * '0', 1' is the set value '0', 2 is the set value '1/1000', 3 is the set value '2/1000' etc.
     *
     * @param p the period to change
     * @param value the int value as described above.
     * @eturn the new Period object.
     */
    private Period setTimeUnitInternalValue(TimeUnit unit, int value) {
        int ord = unit.ordinal;
        if (counts[ord] != value) {
            int[] newCounts = new int[counts.length];
            for (int i = 0; i < counts.length; ++i) {
                newCounts[i] = counts[i];
            }
            newCounts[ord] = value;
            return new Period(timeLimit, inFuture, newCounts);
        }
        return this;
    }

    /**
     * Sets whether this defines a future time.
     *
     * @param future true if the time is in the future
     * @return the new Period
     */
    private Period setFuture(boolean future) {
        if (this.inFuture != future) {
            return new Period(timeLimit, future, counts);
        }
        return this;
    }

    /**
     * Sets whether this is more than, less than, or 'about' the specified time.
     *
     * @param limit the kind of limit
     * @return the new Period
     */
    private Period setTimeLimit(byte limit) {
        if (this.timeLimit != limit) {
            return new Period(limit, inFuture, counts);
        }
        return this;
    }

    /** Validate count. */
    private static void checkCount(float count) {
        if (count < 0) {
            throw new IllegalArgumentException("count (" + count + ") cannot be negative");
        }
    }
}
